﻿using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using LogikBug.ModularMvc.Properties;

namespace LogikBug.ModularMvc.Factories
{
	public class DynamicModelFactory : ModelFactory
	{
		#region Fields



		#endregion

		#region Constructors



		#endregion

		#region Properties

		

		#endregion

		#region Methods

		public override object Create(Type interfaceType)
		{
			if (Cache.Has(interfaceType))
			{
				var type = Cache.GetValueOrDefault<Type>(interfaceType);
				return Activator.CreateInstance(type);
			}

			var dynamicType = GetDynamicType(interfaceType);
			dynamicType.AddInterfaceImplementation(interfaceType);
			dynamicType.DefineDefaultConstructor(MethodAttributes.Public);

			var newType = CreateType(interfaceType, dynamicType);
			Cache.Add(interfaceType, newType);
			return Activator.CreateInstance(newType);
		}

		protected TypeBuilder GetDynamicType(Type interfaceType)
		{
			ModuleBuilder dynamicModule;

			if (Cache.Has(typeof(ModuleBuilder)))
			{
				dynamicModule = Cache.Get<ModuleBuilder>();
			}
			else
			{
				var assemblyName = new AssemblyName(Resources.DynamicAssemblyName);
				var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
				dynamicModule = assemblyBuilder.DefineDynamicModule(assemblyName.Name);

				Cache.Add(typeof(ModuleBuilder), dynamicModule);
			}

			return dynamicModule.DefineType(
				GetDynamicTypeName(interfaceType, Resources.DynamicTypeSuffix),
				TypeAttributes.Public | TypeAttributes.Class |
				TypeAttributes.AutoClass | TypeAttributes.AnsiClass |
				TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
				typeof(object), new[] { interfaceType }
			);
		}

		protected Type CreateType(Type baseType, TypeBuilder dynamicType)
		{
			DefineMethods(baseType, dynamicType);
			DefineProperties(baseType, dynamicType);

			foreach (var @interface in baseType.GetInterfaces())
			{
				DefineProperties(@interface, dynamicType);
				DefineMethods(@interface, dynamicType);
			}

			return dynamicType.CreateType();
		}

		protected void DefineMethods(Type baseType, TypeBuilder dynamicType)
		{
			foreach (var methodInfo in baseType.GetMethods().Where(m => !m.IsSpecialName))
			{
				CreateMethods(methodInfo, dynamicType);
			}
		}

		protected void CreateMethods(MethodInfo methodInfo, TypeBuilder dynamicType)
		{
			var methodBuilder = dynamicType.DefineMethod(
				methodInfo.Name,
				MethodAttributes.Public | MethodAttributes.Virtual,
				CallingConventions.HasThis,
				methodInfo.ReturnType,
				methodInfo.GetParameters().Select(p => p.ParameterType).ToArray()
			);

			var methodGenerator = methodBuilder.GetILGenerator();
			methodGenerator.ThrowException(typeof(NotImplementedException));
		}

		protected void DefineProperties(Type baseType, TypeBuilder dynamicType)
		{
			foreach (var propertyInfo in baseType.GetProperties())
			{
				CreateProperty(propertyInfo, dynamicType);
			}
		}

		protected TypeBuilder CreateProperty(PropertyInfo propertyInfo, TypeBuilder typeBuilder)
		{
			const MethodAttributes getSetAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName;

			var propertyBuilder = typeBuilder.DefineProperty(
				propertyInfo.Name,
				PropertyAttributes.HasDefault,
				CallingConventions.HasThis,
				propertyInfo.PropertyType,
				new[] { propertyInfo.PropertyType }
			);

			var fieldBuilder = typeBuilder.DefineField(
				string.Concat("_", propertyInfo.Name.ToLower()),
				propertyInfo.PropertyType, FieldAttributes.Private
			);

			if (propertyInfo.CanRead)
			{
				CreateGetMethod(typeBuilder, propertyBuilder, propertyInfo.GetGetMethod(), fieldBuilder, getSetAttributes);
			}
			
			if (propertyInfo.CanWrite)
			{
				CreateSetMethod(typeBuilder, propertyBuilder, propertyInfo.GetSetMethod(), fieldBuilder, getSetAttributes);
			}

			return typeBuilder;
		}

		protected void CreateGetMethod(TypeBuilder baseType, PropertyBuilder property, MethodInfo method, FieldInfo field, MethodAttributes getAttrs)
		{
			var getMethodBuilder = baseType.DefineMethod(
				method.Name,
				getAttrs,
				method.ReturnType,
				Type.EmptyTypes
			);

			var getGenerator = getMethodBuilder.GetILGenerator();
			getGenerator.Emit(OpCodes.Ldarg_0);
			getGenerator.Emit(OpCodes.Ldfld, field);
			getGenerator.Emit(OpCodes.Ret);

			property.SetGetMethod(getMethodBuilder);
		}

		protected void CreateSetMethod(TypeBuilder baseType, PropertyBuilder property, MethodInfo method, FieldInfo field, MethodAttributes setAttrs)
		{
			var setMethodBuilder = baseType.DefineMethod(
				method.Name,
				setAttrs,
				method.ReturnType,
				new[] { field.FieldType }
			);

			var setGenerator = setMethodBuilder.GetILGenerator();
			setGenerator.Emit(OpCodes.Ldarg_0);
			setGenerator.Emit(OpCodes.Ldarg_1);
			setGenerator.Emit(OpCodes.Stfld, field);
			setGenerator.Emit(OpCodes.Ret);

			property.SetSetMethod(setMethodBuilder);
		}

		protected string GetDynamicTypeName(Type baseType, string suffix)
		{
			return string.Concat(baseType.FullName, suffix);
		}

		#endregion
	}
}
